package com.springboot.demo.lock.redis;

import java.util.Collections;

import com.springboot.common.util.StringUtil;

import redis.clients.jedis.Jedis;

/**
 * redis分布式锁
 *
 * @author supu
 * @date 2019-11-28 14:00
 **/
public class RedisLock {

    private static final String LOCK_SUCCESS = "OK";
    private static final String SET_IF_NOT_EXIST = "NX";
    /**
     * PX 为 毫秒，EX 是 秒
     */
    private static final String SET_WITH_EXPIRE_TIME = "PX";

    private static final Long RELEASE_SUCCESS = 1L;

    private static final Long SUCCESS = 1L;

    /**
     * 尝试获取分布式锁
     *
     * @param jedis      Redis客户端
     * @param lockKey    锁
     * @param requestId  请求标识
     * @param expireTime 超期时间
     * @return 是否获取成功
     */
    public static boolean tryGetDistributedLock(Jedis jedis, String lockKey, String requestId, int expireTime) {

        String result = jedis.set(lockKey, requestId, SET_IF_NOT_EXIST, SET_WITH_EXPIRE_TIME, expireTime);

        if (LOCK_SUCCESS.equals(result)) {
            return true;
        }
        return false;
    }

    /**
     * 释放分布式锁
     *
     * @param jedis     Redis客户端
     * @param lockKey   锁
     * @param requestId 请求标识
     * @return 是否释放成功
     */
    public static boolean releaseDistributedLock(Jedis jedis, String lockKey, String requestId) {
        // 使用lua脚本保证删除 key 的原子性操作
        String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
        Object result = jedis.eval(script, Collections.singletonList(lockKey), Collections.singletonList(requestId));

        if (RELEASE_SUCCESS.equals(result)) {
            return true;
        }
        return false;
    }

    /**
     * 本方法实现的分布式锁有以下几个问题？这种方法是一个隐藏的坑，建议不要使用
     * <p>
     * 1、System.currentTimeMillis() 都是在客户端的，这个需要保证所有客户端的时间同步
     * <p>
     * 2、没有客户端标识，任何客户端都可以解锁
     * <p>
     * 3、多个客户端都执行到 jedis.getSet 时，那么虽然最终只有一个客户端可以加锁，但是这个客户端的锁的过期时间可能被其他客户端覆盖
     *
     * @param jedis      Redis客户端
     * @param lockKey    锁
     * @param expireTime 过期时间
     * @return
     */
    public static boolean tryLock(Jedis jedis, String lockKey, int expireTime) {
        long expires = System.currentTimeMillis() + expireTime;
        if (SUCCESS.equals(jedis.setnx(lockKey, String.valueOf(expires)))) {
            // 设置成功，表示获取锁成功
            return true;
        }
        // 获取锁的当前值
        String currentValue = jedis.get(lockKey);
        if (StringUtil.isNotEmpty(currentValue) && Long.parseLong(currentValue) < System.currentTimeMillis()) {
            // 时间已过期,即锁已过期，重新计算value后设置到 redis 中，并返回旧值
            long newValue = System.currentTimeMillis() + expireTime;
            String preValue = jedis.getSet(lockKey, String.valueOf(newValue));
            if (currentValue.equals(preValue)) {
                // 设置后返回的旧值和获取锁的当前值相等，表示这期间没有其他线程获取到锁
                return true;
            }
        }

        // 没有获取到锁，直接返回
        return false;
    }
}
